多级菜单:自动展开功能(嵌套与递归通用写法)
修复子菜单的根路径判断问题
在处理多级菜单时,一个常见的 Bug 是:当用户选择了子菜单项(而非顶级菜单项)时,左侧菜单无法正确显示对应的二级菜单列表。问题的根源在于 getSubMenu 方法中对根路径的判断逻辑不正确。
错误的路径判断
原先的代码直接使用 route.path 作为判断条件,但对于嵌套路由(如 /components/icon/icon-picker),route.path 返回的是完整路径,而非顶级路径。因此需要从完整路径中提取出根路径:
// composables/useMenu.ts
function getRootPath(path: string): string {
if (path === '/') return '/'
const arr = path.split('/')
const rootPath = arr[1] || ''
return rootPath ? `/${rootPath}` : '/'
}
typescript
这个函数通过 split('/') 将路径打散为数组,取第一个非空元素作为根路径,并处理了根路径 / 的特殊情况。
通用递归查找方法
本节将上一节的 getItem 方法改造为更通用的 getItemByCondition,接受一个回调函数作为匹配条件,而不是硬编码的 index 比较:
// composables/useMenu.ts
/**
* 通用递归查找:根据自定义条件在菜单树中查找项
* @param menus 菜单树
* @param fn 匹配条件函数
*/
function getItemByCondition(
menus: AppRouteMenuItem[],
fn: (item: AppRouteMenuItem) => boolean
): AppRouteMenuItem | undefined {
for (let i = 0; i < menus.length; i++) {
if (fn(menus[i])) {
return menus[i]
}
if (menus[i].children && Array.isArray(menus[i].children)) {
const item = getItemByCondition(menus[i].children!, fn)
if (item) return item
}
}
return undefined
}
// 基于 getItemByCondition 的便捷方法
function getItem(
menus: AppRouteMenuItem[],
index: string
): AppRouteMenuItem | undefined {
return getItemByCondition(menus, (item) => item.meta?.key === index)
}
typescript
这种设计的核心思想是将遍历逻辑与匹配逻辑解耦:递归遍历的骨架是固定的,而匹配条件由调用者通过回调函数灵活定义。
页面刷新时自动展开父级菜单
当用户在子菜单页面刷新浏览器时,左侧菜单需要自动展开对应的父级菜单。这需要三个步骤:
步骤一:查找父级菜单
利用 getItemByCondition,根据当前路由路径查找父级菜单项:
function getParentMenu(
menus: AppRouteMenuItem[]
): AppRouteMenuItem | undefined {
const route = useRoute()
const activePath = computed(() => route.path)
return getItemByCondition(menus, (item) => {
const arr = activePath.value.split('/')
// 路径层级不足时,不存在父级菜单
if (arr.length < 3) return false
// 移除最后一个元素,得到父级路径
arr.pop()
// item.name 是路由自动生成的完整路径名
return arr.join('/') === item.name
})
}
typescript
这里需要注意一个细节:比较时使用的是 item.name 而非 item.path。item.path 是子级页面的路径,而 item.name 是路由系统自动生成的、拼接了父级路径的完整标识符。
步骤二:获取 Menu 组件的 ref 引用
为了调用 Element Plus el-menu 暴露的 open 方法,需要通过 ref 获取组件实例:
<!-- Menu.vue -->
<script setup lang="ts">
const menuRef = ref()
// 将 menuRef 绑定到 el-menu 组件
</script>
<template>
<el-menu ref="menuRef">
<!-- menu items -->
</el-menu>
</template>
vue
步骤三:在 onMounted 中自动展开
组件挂载后,查找当前页面的父级菜单,并调用 open 方法展开:
// Menu.vue
onMounted(() => {
const item = getParentMenu(filteredMenu.value)
if (item?.meta?.key && menuRef.value?.open) {
menuRef.value.open(item.meta.key)
}
})
typescript
Element Plus 的 el-menu.open(index) 方法接收 submenu 的 index 值(即 meta.key),会展开对应的子菜单。
Grid 布局的尺寸调整
在实现多级菜单功能的过程中,还修复了图标列表页面的 Grid 布局问题。原先设置的 1.825rem 最小宽度太小,导致图标在折叠状态下显示不全。调整为 3rem 的最小值和 7rem 的最大值后,图标列表在展开和折叠两种状态下都能正常显示。
/* 图标列表 Grid 布局 */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(3rem, 7rem));
gap: 0.5rem;
}
css
关键要点总结
- 路径解析 是多级菜单的基础,需要从完整路径中正确提取根路径,处理根路径
/的边界情况 - 通用递归查找
getItemByCondition将遍历逻辑与匹配条件解耦,是一个可复用的基础工具方法,支持按 key、按 path、按任意条件查找 - 自动展开父级菜单 的核心流程是:获取当前路径 -> 提取父级路径 -> 递归查找父级菜单项 -> 调用
el-menu.open()方法 item.namevsitem.path:name 是路由系统生成的完整标识符,path 是配置中的路径字段,在比较时需要选择正确的属性
↑